/// @file
/// @brief Contains tunit::unit_test class.
#pragma once

#include "event_listener.h"
#include "registered_test_class.h"
#include "settings.h"
#include <chrono>
#include <iomanip>
#include <fstream>
#include <memory>
#include <random>
#include <string>

/// @brief The tunit namespace contains a unit test library.
namespace tunit {
  /// @cond
  template <typename TestClass>
  class test_class_attribute;
  /// @endcond

  /// @brief The template class.
  class unit_test {
  public:
    explicit unit_test(std::unique_ptr<tunit::event_listener> event_listener) noexcept;
    
    unit_test(std::unique_ptr<tunit::event_listener> event_listener, char* argv[], int argc) noexcept : arguments(argv + 1, argv + argc), name_(get_filename(argv[0])), event_listener_(std::move(event_listener)) {}

    /// @cond
    virtual ~unit_test() {}
    /// @endcond
    
    /// @brief Runs all tests in this UnitTest object and prints the result.
    /// @return EXIT_SUCCESS (0) if succeed; otherwise return EXIT_FAILURE (1).
    int run() {
      if (parse_arguments(this->arguments))
        return tunit::settings::default_settings().exit_status();

      if (tunit::settings::default_settings().list_tests()) {
        std::vector<std::string> tests;
        for (auto test_class : test_classes())
          for(auto test : test_class.test()->tests())
            tests.push_back(test_class.test()->name() + '.' + test.name());
        return this->list_tests(tests);
      }

      if (tunit::settings::default_settings().shuffle_test()) {
        std::random_device rd;
        std::mt19937 g = tunit::settings::default_settings().random_seed() == 0 ? std::mt19937(rd()) : std::mt19937(tunit::settings::default_settings().random_seed());
        std::shuffle(test_classes().begin(), test_classes().end(), g);
      }
        
      for (this->repeat_iteration_ = 1; this->repeat_iteration_ <= tunit::settings::default_settings().repeaat_test() || tunit::settings::default_settings().repeaat_test() < 0; ++this->repeat_iteration_) {
         try {
          this->event_listener_->on_unit_test_start(tunit::tunit_event_args(*this));
          
          this->event_listener_->on_unit_test_initialize_start(tunit::tunit_event_args(*this));
          unit_test_initialize();
          this->event_listener_->on_unit_test_initialize_end(tunit::tunit_event_args(*this));
          
          this->start_time_point_ = std::chrono::high_resolution_clock::now();
          for (auto& test_class : test_classes())
            if (test_class.test()->test_count())
              test_class.test()->run(*this);
          this->end_time_point_ = std::chrono::high_resolution_clock::now();

          this->event_listener_->on_unit_test_cleanup_start(tunit::tunit_event_args(*this));
          unit_test_cleanup();
          this->event_listener_->on_unit_test_cleanup_end(tunit::tunit_event_args(*this));
          
          this->event_listener_->on_unit_test_end(tunit::tunit_event_args(*this));
        } catch(const std::exception&) {
          tunit::settings::default_settings().exit_status(EXIT_FAILURE);
          // do error...
        } catch(...) {
          tunit::settings::default_settings().exit_status(EXIT_FAILURE);
          // do error...
        }
      }

      tunit::settings::default_settings().end_time(std::chrono::system_clock::now());
      write_xml();
      
      return tunit::settings::default_settings().exit_status();
    }
    
    int repeat_iteration() const noexcept {return this->repeat_iteration_;}
    
    int repeat_iteration_count() const noexcept {return tunit::settings::default_settings().repeaat_test();}
    
    bool repeat_tests() const noexcept {return tunit::settings::default_settings().repeaat_test() != 1;}
    
    size_t test_cases_count() const noexcept {
      size_t count = 0;
      for (auto test_class : this->test_classes())
        if (test_class.test()->test_count())
          count ++;
      return count;
    }
    
    size_t test_count() const noexcept {
      size_t count = 0;
      for (auto test_class : this->test_classes())
        count += test_class.test()->test_count();
      return count;
    }

    size_t aborted_test_count() const noexcept {
      size_t count = 0;
      for (auto& test_class : this->test_classes())
        for (auto& test : test_class.test()->tests())
          if (settings::default_settings().is_match_test_name(test_class.test()->name(), test.name()) && test.aborted()) count++;
      return count;
    }
    
    std::vector<std::string> aborted_test_names() const noexcept {
      std::vector<std::string> names;
      for (auto& test_class : this->test_classes())
        for (auto& test : test_class.test()->tests())
          if (settings::default_settings().is_match_test_name(test_class.test()->name(), test.name()) && test.aborted()) names.push_back(test_class.test()->name() + "." + test.name());
      return names;
    }

    std::chrono::milliseconds elapsed_time() const noexcept {
      using namespace std::chrono_literals;
      if (this->start_time_point_.time_since_epoch() == 0ms && this->end_time_point_.time_since_epoch() == 0ms) return 0ms;
      if (this->end_time_point_.time_since_epoch() == 0ms) return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - this->start_time_point_);
      return std::chrono::duration_cast<std::chrono::milliseconds>(this->end_time_point_ - this->start_time_point_);
    }

    size_t ignored_test_count() const noexcept {
      size_t count = 0;
      for (auto test_class : this->test_classes())
        count += test_class.test()->ignored_test_count();
      return count;
    }

    std::vector<std::string> ignored_test_names() const noexcept {
      std::vector<std::string> names;
      for (auto& test_class : this->test_classes())
        for (auto& test : test_class.test()->tests())
          if (settings::default_settings().is_match_test_name(test_class.test()->name(), test.name()) && test.ignored()) names.push_back(test_class.test()->name() + "." + test.name());
      return names;
    }

    size_t failed_test_count() const noexcept {
      size_t count = 0;
      for (auto& test_class : this->test_classes())
        for (auto& test : test_class.test()->tests())
          if (settings::default_settings().is_match_test_name(test_class.test()->name(), test.name()) && test.failed()) count++;
      return count;
    }

    std::vector<std::string> failed_test_names() const noexcept {
      std::vector<std::string> names;
      for (auto& test_class : this->test_classes())
        for (auto& test : test_class.test()->tests())
          if (settings::default_settings().is_match_test_name(test_class.test()->name(), test.name()) && test.failed()) names.push_back(test_class.test()->name() + "." + test.name());
      return names;
    }

    size_t succeed_test_count() const noexcept {
      size_t count = 0;
      for (auto& test_class : this->test_classes())
        for (auto& test : test_class.test()->tests())
          if (settings::default_settings().is_match_test_name(test_class.test()->name(), test.name()) && test.succeed()) count++;
      return count;
    }

    std::vector<std::string> succeed_test_names() const noexcept {
      std::vector<std::string> names;
      for (auto& test_class : this->test_classes())
        for (auto& test : test_class.test()->tests())
          if (settings::default_settings().is_match_test_name(test_class.test()->name(), test.name()) && test.succeed()) names.push_back(test_class.test()->name() + "." + test.name());
      return names;
    }
    
  protected:
    virtual int list_tests(const std::vector<std::string>& tests) {
      return tunit::settings::default_settings().exit_status();
    }
    
    virtual bool parse_arguments(const std::vector<std::string>& args) {
      for (auto arg : args) {
        if (arg == "--also_run_ignored_tests") tunit::settings::default_settings().also_run_ignored_tests(true);
        else if (arg.find("--filter_tests=") == 0) tunit::settings::default_settings().filter_tests(arg.substr(15));
        else if (arg == "--list_tests") tunit::settings::default_settings().list_tests(true);
        else if (arg == "--output_color=true") tunit::settings::default_settings().output_color(true);
        else if (arg == "--output_color=false") tunit::settings::default_settings().output_color(false);
        else if (arg.find("--output_xml") == 0) { tunit::settings::default_settings().output_xml(true);
          if (arg[12] == '=') tunit::settings::default_settings().output_xml_path(arg.substr(13));
        } else if (arg.find("--random_seed=") == 0) tunit::settings::default_settings().random_seed(std::stoi(arg.substr(14)));
        else if (arg.find("--repeat_tests=") == 0) tunit::settings::default_settings().repeat_tests(std::stoi(arg.substr(15)));
        else if (arg == "--show_duration=true") tunit::settings::default_settings().show_duration(true);
        else if (arg == "--show_duration=false") tunit::settings::default_settings().show_duration(false);
        else if (arg == "--shuffle_tests") tunit::settings::default_settings().shuffle_test(true);
      }
      return false;
    }

  private:
    template <typename TestClass>
    friend class tunit::test_class_attribute;
    friend class tunit::test_class;
    friend class tunit::test;
    friend class tunit::base_assert;

    static void add(const tunit::registered_test_class& test_class) {test_classes().push_back(test_class);}
    
    void unit_test_cleanup() {
    }
    
    void unit_test_initialize() {
    }
    
    static std::vector<tunit::registered_test_class>& test_classes() {
      static std::vector<tunit::registered_test_class> test_classes;
      return test_classes;
    }
    
    std::string get_filename(const std::string& path) {
      std::string filename = path;
      const size_t last_slash_idx = filename.find_last_of("\\/");
      if (std::string::npos != last_slash_idx)
        filename.erase(0, last_slash_idx + 1);
      
      const size_t period_idx = filename.rfind('.');
      if (std::string::npos != period_idx)
        filename.erase(period_idx);
      return filename;
    }
    
    std::string to_string(const std::chrono::milliseconds& ms) {
      std::stringstream ss;
      if (ms.count() == 0)
        ss << 0;
      else
        ss << ms.count() / 1000 << "." << std::setfill('0') << std::setw(3) << ms.count() % 1000;
      return ss.str();
    }
    
    std::string to_string(const std::chrono::time_point<std::chrono::system_clock>& time) {
      std::time_t time_t = std::chrono::system_clock::to_time_t(time);
      std::tm tm = *std::localtime(&time_t);
      std::stringstream ss;
      ss << tm.tm_year + 1900 << "-" << std::setfill('0') << std::setw(2) << tm.tm_mon << "-" << std::setfill('0') << std::setw(2) << tm.tm_mday;
      ss << "T" << std::setfill('0') << std::setw(2) << tm.tm_hour << ":" << std::setfill('0') << std::setw(2) << tm.tm_min << ":" << std::setfill('0') << std::setw(2) << tm.tm_sec;
      return ss.str();
    }

    std::string status_to_string(const tunit::test& test) {
      std::stringstream ss;
      if (test.not_started() || test.ignored()) ss << "notrun";
      else ss << "run";
      return ss.str();
    }
    
    std::string message_to_string(const tunit::test& test) {
      std::stringstream ss;
      if (test.line_info() != tunit::line_info::empty())
        ss << test.line_info().file_path() << ":" << test.line_info().line_number() << "&#0x0A;";
      ss << "Expected: " << test.expect() << "&#0x0A;";
      ss << "But was : " << test.actual();
      return ss.str();
    }
    
    std::string cdata_message_to_string(const tunit::test& test) {
      std::stringstream ss;
      if (test.line_info() != tunit::line_info::empty())
        ss << test.line_info().file_path() << ":" << test.line_info().line_number() << std::endl;
      ss << "Expected: " << test.expect() << std::endl;
      ss << "But was : " << test.actual();
      return ss.str();
    }
    
    void write_xml() {
      if (tunit::settings::default_settings().output_xml()) {
        std::fstream file(tunit::settings::default_settings().output_xml_path(), std::ios::out | std::ios::trunc);
        file << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << std::endl;
        file << "<testsuites tests=\"" << this->test_count() << "\" failures=\"" << this->failed_test_count() << "\" disabled=\"" << this->ignored_test_count() << "\" errors=\"" << 0 << "\" timestamp=\"" << to_string(tunit::settings::default_settings().start_time()) << "\" time=\"" << to_string(this->elapsed_time()) << "\" name=\"" << this->name_ << "\">" << std::endl;
        for (auto& test_class : test_classes()) {
          file << "  <testsuite name=\"" << test_class.test()->name() << "\" tests=\"" << test_class.test()->test_count() << "\" failures=\"" << test_class.test()->failed_test_count() << "\" disabled=\"" << test_class.test()->ignored_test_count() << "\" error=\"" << test_class.test()->failed_test_count() << "\" time=\"" << to_string(test_class.test()->elapsed_time()) << "\">" << std::endl;
          for (auto& test : test_class.test()->tests()) {
            file << "    <testcase name=\"" << test.name() << "\" status=\"" << status_to_string(test) << "\" time=\"" << to_string(test.elapsed_time()) << "\" classname=\"" << test_class.test()->name() << "\"";
            if (!test.failed())
              file << " />" << std::endl;
            else {
              file << ">" << std::endl;
              file << "      <failure message=\"" << message_to_string(test) << "\" type= \"" << "\">" << "<![CDATA[" << cdata_message_to_string(test) << "]]></failure>" << std::endl;
              file << "    </testcase>" << std::endl;
            }
          }
          file << "  </testsuite>" << std::endl;
        }
        file << "</testsuites>" << std::endl;
        file.close();
      }
    }

    std::vector<std::string> arguments;
    std::string name_ = "AllTests";
    std::unique_ptr<tunit::event_listener> event_listener_;
    std::chrono::high_resolution_clock::time_point end_time_point_;
    int repeat_iteration_ = 0;
    std::chrono::high_resolution_clock::time_point start_time_point_;
  };
}
